Table of Contents

  • 1  Загружаем данные и подготавливаем их к анализу
  • 2  Задаем функции для расчёта и анализа LTV, ROI, удержания и конверсии.
  • 3  Исследовательский анализ данных
  • 4  Маркетинг
  • 5  Оценим окупаемость рекламы
  • 6  Напишите выводы

Маркетинговый анализ приложения Procrastinate Pro+¶

Для развлекательного приложения Procrastinate Pro+ необходимо провести маркетинговый анализ. Выявить причины убытков в последние несколько месяцев, несмотря на огромные вложения в рекламу. Нам надо разобраться в причинах и помочь компании выйти в плюс.

Были предоставлены лог сервера с данными об их посещениях, привлечённых с 1 мая по 27 октября 2019 года с выгрузкой их покупок за этот период и рекламные расходы.

Изучим откуда приходят пользователи и какими устройствами они пользуются, сколько стоит привлечение пользователей из различных рекламных каналов , сколько денег приносит каждый клиент, когда расходы на привлечение клиента окупаются, какие факторы мешают привлечению клиентов.

Описание данных

В вашем распоряжении три датасета. Файл visits_info_short.csv хранит лог сервера с информацией о посещениях сайта, orders_info_short.csv — информацию о заказах, а costs_info_short.csv — информацию о расходах на рекламу.

Структура visits_info_short.csv:

  • User Id — уникальный идентификатор пользователя,
  • Region — страна пользователя,
  • Device — тип устройства пользователя,
  • Channel — идентификатор источника перехода,
  • Session Start — дата и время начала сессии,
  • Session End — дата и время окончания сессии.

Структура orders_info_short.csv:

  • User Id — уникальный идентификатор пользователя,
  • Event Dt — дата и время покупки,
  • Revenue — сумма заказа.

Структура costs_info_short.csv:dt — дата проведения рекламной кампании,

  • Channel — идентификатор рекламного источника,
  • costs — расходы на эту кампанию.

Загружаем данные и подготавливаем их к анализу¶

In [1]:
# импортируем библиотеки 
import pandas as pd
from datetime import datetime, timedelta
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
In [2]:
# загружаем данные о визитах, заказах и рекламных расходах из CSV-файлов в переменные.
try:
    visits = pd.read_csv(r"B:\Downloads\visits_info_short.csv", sep = ',') # лог сервера с информацией о посещениях сайта

    orders = pd.read_csv(r"B:\Downloads\orders_info_short.csv", sep = ',') # информацию о заказах
    costs = pd.read_csv(r"B:\Downloads\costs_info_short.csv", sep = ',') # информацию о расходах на рекламу
    
except:
    visits = pd.read_csv("/datasets/visits_info_short.csv", sep = ',')
    orders = pd.read_csv("/datasets/orders_info_short.csv", sep = ',')
    costs = pd.read_csv("/datasets/costs_info_short.csv", sep = ',')
In [3]:
#выведем 10 строк таблицы visits, orders, costs
display(visits.head(10))
display(orders.head(10))
costs.head(10)
User Id Region Device Channel Session Start Session End
0 981449118918 United States iPhone organic 2019-05-01 02:36:01 2019-05-01 02:45:01
1 278965908054 United States iPhone organic 2019-05-01 04:46:31 2019-05-01 04:47:35
2 590706206550 United States Mac organic 2019-05-01 14:09:25 2019-05-01 15:32:08
3 326433527971 United States Android TipTop 2019-05-01 00:29:59 2019-05-01 00:54:25
4 349773784594 United States Mac organic 2019-05-01 03:33:35 2019-05-01 03:57:40
5 90242400005 United States iPhone RocketSuperAds 2019-05-01 07:14:52 2019-05-01 08:19:56
6 43958116050 United States Android organic 2019-05-01 09:03:21 2019-05-01 10:08:39
7 185365244969 United States iPhone organic 2019-05-01 09:37:03 2019-05-01 10:00:43
8 446013509831 United States iPhone RocketSuperAds 2019-05-01 10:26:00 2019-05-01 10:59:17
9 100970711362 United States Mac TipTop 2019-05-01 04:39:53 2019-05-01 04:57:05
User Id Event Dt Revenue
0 188246423999 2019-05-01 23:09:52 4.99
1 174361394180 2019-05-01 12:24:04 4.99
2 529610067795 2019-05-01 11:34:04 4.99
3 319939546352 2019-05-01 15:34:40 4.99
4 366000285810 2019-05-01 13:59:51 4.99
5 129100331057 2019-05-01 01:56:28 4.99
6 626807144131 2019-05-01 02:39:21 4.99
7 914062168985 2019-05-01 02:02:18 4.99
8 769669137453 2019-05-01 03:39:47 4.99
9 434198630691 2019-05-01 18:16:50 5.99
Out[3]:
dt Channel costs
0 2019-05-01 FaceBoom 113.3
1 2019-05-02 FaceBoom 78.1
2 2019-05-03 FaceBoom 85.8
3 2019-05-04 FaceBoom 136.4
4 2019-05-05 FaceBoom 122.1
5 2019-05-06 FaceBoom 118.8
6 2019-05-07 FaceBoom 101.2
7 2019-05-08 FaceBoom 100.1
8 2019-05-09 FaceBoom 93.5
9 2019-05-10 FaceBoom 104.5
In [4]:
# выведем количество строк и столбцов в таблицах
display(visits.shape)
display(orders.shape)
costs.shape        
(309901, 6)
(40212, 3)
Out[4]:
(1800, 3)
In [5]:
# выведем статистические показатели в таблицах visits, orders, costs
display(orders.describe())
display(visits.describe())
costs.describe()  
User Id Revenue
count 4.021200e+04 40212.000000
mean 4.990295e+11 5.370608
std 2.860937e+11 3.454208
min 5.993260e+05 4.990000
25% 2.511324e+11 4.990000
50% 4.982840e+11 4.990000
75% 7.433327e+11 4.990000
max 9.998954e+11 49.990000
User Id
count 3.099010e+05
mean 4.997664e+11
std 2.887899e+11
min 5.993260e+05
25% 2.493691e+11
50% 4.989906e+11
75% 7.495211e+11
max 9.999996e+11
Out[5]:
costs
count 1800.000000
mean 58.609611
std 107.740223
min 0.800000
25% 6.495000
50% 12.285000
75% 33.600000
max 630.000000
In [6]:
# выведем общую информацию о данных в таблицах visits, orders, costs
display(orders.info())
display(visits.info())
costs.info()  
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40212 entries, 0 to 40211
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   User Id   40212 non-null  int64  
 1   Event Dt  40212 non-null  object 
 2   Revenue   40212 non-null  float64
dtypes: float64(1), int64(1), object(1)
memory usage: 942.6+ KB
None
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 309901 entries, 0 to 309900
Data columns (total 6 columns):
 #   Column         Non-Null Count   Dtype 
---  ------         --------------   ----- 
 0   User Id        309901 non-null  int64 
 1   Region         309901 non-null  object
 2   Device         309901 non-null  object
 3   Channel        309901 non-null  object
 4   Session Start  309901 non-null  object
 5   Session End    309901 non-null  object
dtypes: int64(1), object(5)
memory usage: 14.2+ MB
None
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1800 entries, 0 to 1799
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   dt       1800 non-null   object 
 1   Channel  1800 non-null   object 
 2   costs    1800 non-null   float64
dtypes: float64(1), object(2)
memory usage: 42.3+ KB
In [7]:
# посчитаем количество дубликатов в таблицах visits, orders, costs
display(visits.duplicated().sum())
display(orders.duplicated().sum())
costs.duplicated().sum()
0
0
Out[7]:
0
In [8]:
# заменим пробелы в названиях столбцов на нижнее подчеркивание в таблицах visits, orders, costs
visits.columns = [x.lower().replace(' ', '_') for x in visits.columns]
orders.columns = [x.lower().replace(' ', '_') for x in orders.columns]
costs.columns = costs.columns.str.lower() 
visits.columns, orders.columns, costs.columns
Out[8]:
(Index(['user_id', 'region', 'device', 'channel', 'session_start',
        'session_end'],
       dtype='object'),
 Index(['user_id', 'event_dt', 'revenue'], dtype='object'),
 Index(['dt', 'channel', 'costs'], dtype='object'))
In [9]:
# уникальные значения столбца region
visits.region.value_counts()
Out[9]:
United States    207327
UK                36419
France            35396
Germany           30759
Name: region, dtype: int64
In [10]:
# уникальные значения столбца device
visits.device.value_counts()
Out[10]:
iPhone     112603
Android     72590
PC          62686
Mac         62022
Name: device, dtype: int64
In [11]:
# уникальные значения столбца channel
visits.channel.value_counts()
Out[11]:
organic               107760
TipTop                 54794
FaceBoom               49022
WahooNetBanner         20465
LeapBob                17013
OppleCreativeMedia     16794
RocketSuperAds         12724
YRabbit                 9053
MediaTornado            8878
AdNonSense              6891
lambdaMediaAds          6507
Name: channel, dtype: int64
In [12]:
# приводим данные о времени к формату datetime
visits['session_start'] = pd.to_datetime(visits['session_start'])
orders['event_dt'] = pd.to_datetime(orders['event_dt'])
costs['dt'] = pd.to_datetime(costs['dt']).dt.date 

Для исследования загрузили 3 таблицы:

    1. visits состоит из 309901 строк и 6 столбцов с информацией о визитах
    1. orders состоит из 40212 строк и 3 столбцов с информацией о заказах
    1. costs состоит из 1800 строк и 3 столбцов с информацией о рекламных расходах.

В анализе учавствуют 4 страны: United States(США), UK(Великобритания), France(Франция), Germany(Германия), разделенные на категории устройств пользователей iPhone, Android, PC, Mac на разных каналах.

Дубликаты и пропуски в данных отсутствуют. Заменили в таблицах пробелы в названиях столбцов на нижнее подчеркивание и перевели в нижний регистр. Преобразовали данные о времени в таблицах к формату datetime.

Задаем функции для расчёта и анализа LTV, ROI, удержания и конверсии.¶

Задаем функции для вычисления значений метрик:

In [13]:
# функция для создания пользовательских профилей

def get_profiles(sessions, orders, ad_costs, event_names=[]):

    # находим параметры первых посещений
    profiles = (
        sessions.sort_values(by=['user_id', 'session_start'])
        .groupby('user_id')
        .agg(
            {
                'session_start': 'first',
                'channel': 'first',
                'device': 'first',
                'region': 'first',
            }
        )
        .rename(columns={'session_start': 'first_ts'})
        .reset_index()
    )

    # для когортного анализа определяем дату первого посещения
    # и первый день месяца, в который это посещение произошло
    profiles['dt'] = profiles['first_ts'].dt.date
    profiles['month'] = profiles['first_ts'].astype('datetime64[M]')

    # добавляем признак платящих пользователей
    profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())

   

    # считаем количество уникальных пользователей
    # с одинаковыми источником и датой привлечения
    new_users = (
        profiles.groupby(['dt', 'channel'])
        .agg({'user_id': 'nunique'})
        .rename(columns={'user_id': 'unique_users'})
        .reset_index()
    )

    # объединяем траты на рекламу и число привлечённых пользователей
    ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')

    # делим рекламные расходы на число привлечённых пользователей
    ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']

    # добавляем стоимость привлечения в профили
    profiles = profiles.merge(
        ad_costs[['dt', 'channel', 'acquisition_cost']],
        on=['dt', 'channel'],
        how='left',
    )

    # стоимость привлечения органических пользователей равна нулю
    profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)

    return profiles
In [14]:
# функция для расчёта Retention Rate(удержания) 
def get_retention(
    profiles,
    sessions,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # добавляем столбец payer в передаваемый dimensions список
    dimensions = ['payer'] + dimensions

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # собираем «сырые» данные для расчёта удержания
    result_raw = result_raw.merge(
        sessions[['user_id', 'session_start']], on='user_id', how='left'
    )
    result_raw['lifetime'] = (
        result_raw['session_start'] - result_raw['first_ts']
    ).dt.days

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу удержания
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # получаем таблицу динамики удержания
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time 
In [15]:
# функция для расчёта конверсии

def get_conversion(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # определяем дату и время первой покупки для каждого пользователя
    first_purchases = (
        purchases.sort_values(by=['user_id', 'event_dt'])
        .groupby('user_id')
        .agg({'event_dt': 'first'})
        .reset_index()
    )

    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
    )

    # рассчитываем лайфтайм для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days

    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users' 
        dimensions = dimensions + ['cohort']

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        result = result.fillna(0).cumsum(axis = 1)
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # делим каждую «ячейку» в строке на размер когорты
        # и получаем conversion rate
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу конверсии
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # для таблицы динамики конверсии убираем 'cohort' из dimensions
    if 'cohort' in dimensions: 
        dimensions = []

    # получаем таблицу динамики конверсии
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time 
In [16]:
# функция для расчёта LTV и ROI

def get_ltv(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
    )
    # рассчитываем лайфтайм пользователя для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days
    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users'
        dimensions = dimensions + ['cohort']

    # функция группировки по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        # строим «треугольную» таблицу выручки
        result = df.pivot_table(
            index=dims, columns='lifetime', values='revenue', aggfunc='sum'
        )
        # находим сумму выручки с накоплением
        result = result.fillna(0).cumsum(axis=1)
        # вычисляем размеры когорт
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        # объединяем размеры когорт и таблицу выручки
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # считаем LTV: делим каждую «ячейку» в строке на размер когорты
        result = result.div(result['cohort_size'], axis=0)
        # исключаем все лайфтаймы, превышающие горизонт анализа
        result = result[['cohort_size'] + list(range(horizon_days))]
        # восстанавливаем размеры когорт
        result['cohort_size'] = cohort_sizes

        # собираем датафрейм с данными пользователей и значениями CAC, 
        # добавляя параметры из dimensions
        cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()

        # считаем средний CAC по параметрам из dimensions
        cac = (
            cac.groupby(dims)
            .agg({'acquisition_cost': 'mean'})
            .rename(columns={'acquisition_cost': 'cac'})
        )

        # считаем ROI: делим LTV на CAC
        roi = result.div(cac['cac'], axis=0)

        # удаляем строки с бесконечным ROI
        roi = roi[~roi['cohort_size'].isin([np.inf])]

        # восстанавливаем размеры когорт в таблице ROI
        roi['cohort_size'] = cohort_sizes

        # добавляем CAC в таблицу ROI
        roi['cac'] = cac['cac']

        # в финальной таблице оставляем размеры когорт, CAC
        # и ROI в лайфтаймы, не превышающие горизонт анализа
        roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]

        # возвращаем таблицы LTV и ROI
        return result, roi

    # получаем таблицы LTV и ROI
    result_grouped, roi_grouped = group_by_dimensions(
        result_raw, dimensions, horizon_days
    )

    # для таблиц динамики убираем 'cohort' из dimensions
    if 'cohort' in dimensions:
        dimensions = []

    # получаем таблицы динамики LTV и ROI
    result_in_time, roi_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    return (
        result_raw,  # сырые данные
        result_grouped,  # таблица LTV
        result_in_time,  # таблица динамики LTV
        roi_grouped,  # таблица ROI
        roi_in_time,  # таблица динамики ROI
    ) 

Задаем функции для построения графиков:

In [17]:
# функция для сглаживания фрейма

def filter_data(df, window):
    # для каждого столбца применяем скользящее среднее
    for column in df.columns.values:
        df[column] = df[column].rolling(window).mean() 
    return df
In [18]:
# функция для визуализации удержания

def plot_retention(retention, retention_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 10))

    # исключаем размеры когорт и удержание первого дня
    retention = retention.drop(columns=['cohort_size', 0])
    # в таблице динамики оставляем только нужный лайфтайм
    retention_history = retention_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # если в индексах таблицы удержания только payer,
    # добавляем второй признак — cohort
    if retention.index.nlevels == 1:
        retention['cohort'] = 'All users'
        retention = retention.reset_index().set_index(['cohort', 'payer'])

    # в таблице графиков — два столбца и две строки, четыре ячейки
    # в первой строим кривые удержания платящих пользователей
    ax1 = plt.subplot(2, 2, 1)
    retention.query('payer == True').droplevel('payer').T.plot(
        grid=True, ax=ax1
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание платящих пользователей')

    # во второй ячейке строим кривые удержания неплатящих
    # вертикальная ось — от графика из первой ячейки
    ax2 = plt.subplot(2, 2, 2, sharey=ax1)
    retention.query('payer == False').droplevel('payer').T.plot(
        grid=True, ax=ax2
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание неплатящих пользователей')

    # в третьей ячейке — динамика удержания платящих
    ax3 = plt.subplot(2, 2, 3)
    # получаем названия столбцов для сводной таблицы
    columns = [
        name
        for name in retention_history.index.names
        if name not in ['dt', 'payer']
    ]
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == True').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания платящих пользователей на {}-й день'.format(
            horizon
        )
    )

    # в чётвертой ячейке — динамика удержания неплатящих
    ax4 = plt.subplot(2, 2, 4, sharey=ax3)
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == False').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax4)
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания неплатящих пользователей на {}-й день'.format(
            horizon
        )
    )
    
    plt.tight_layout()
    plt.show()
In [19]:
# функция для визуализации конверсии

def plot_conversion(conversion, conversion_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 5))

    # исключаем размеры когорт
    conversion = conversion.drop(columns=['cohort_size'])
    # в таблице динамики оставляем только нужный лайфтайм
    conversion_history = conversion_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # первый график — кривые конверсии
    ax1 = plt.subplot(1, 2, 1)
    conversion.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Конверсия пользователей')

    # второй график — динамика конверсии
    ax2 = plt.subplot(1, 2, 2, sharey=ax1)
    columns = [
        # столбцами сводной таблицы станут все столбцы индекса, кроме даты
        name for name in conversion_history.index.names if name not in ['dt']
    ]
    filtered_data = conversion_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()
In [20]:
# функция для визуализации LTV и ROI

def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):

    # задаём сетку отрисовки графиков
    plt.figure(figsize=(20, 10))

    # из таблицы ltv исключаем размеры когорт
    ltv = ltv.drop(columns=['cohort_size'])
    # в таблице динамики ltv оставляем только нужный лайфтайм
    ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]

    # стоимость привлечения запишем в отдельный фрейм
    cac_history = roi_history[['cac']]

    # из таблицы roi исключаем размеры когорт и cac
    roi = roi.drop(columns=['cohort_size', 'cac'])
    # в таблице динамики roi оставляем только нужный лайфтайм
    roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
        [horizon - 1]
    ]

    # первый график — кривые ltv
    ax1 = plt.subplot(2, 3, 1)
    ltv.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('LTV')

    # второй график — динамика ltv
    ax2 = plt.subplot(2, 3, 2, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in ltv_history.index.names if name not in ['dt']]
    filtered_data = ltv_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))

    # третий график — динамика cac
    ax3 = plt.subplot(2, 3, 3, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in cac_history.index.names if name not in ['dt']]
    filtered_data = cac_history.pivot_table(
        index='dt', columns=columns, values='cac', aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика стоимости привлечения пользователей')

    # четвёртый график — кривые roi
    ax4 = plt.subplot(2, 3, 4)
    roi.T.plot(grid=True, ax=ax4)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('ROI')

    # пятый график — динамика roi
    ax5 = plt.subplot(2, 3, 5, sharey=ax4)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in roi_history.index.names if name not in ['dt']]
    filtered_data = roi_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax5)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.xlabel('Дата привлечения')
    plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show() 

Задали функции для:

1.вычисления значений метрик:

  • get_profiles() — для создания профилей пользователей,
  • get_retention() — для подсчёта Retention Rate,
  • get_conversion() — для подсчёта конверсии,
  • get_ltv() — для подсчёта LTV. 2.построения графиков:
  • filter_data() — для сглаживания данных,
  • plot_retention() — для построения графика Retention Rate,
  • plot_conversion() — для построения графика конверсии,
  • plot_ltv_roi — для визуализации LTV и ROI.

Исследовательский анализ данных¶

  • Составьте профили пользователей. Определите минимальную и максимальную даты привлечения пользователей.
  • Выясните, из каких стран пользователи приходят в приложение и на какую страну приходится больше всего платящих пользователей. Постройте таблицу, отражающую количество пользователей и долю платящих из каждой страны.
  • Узнайте, какими устройствами пользуются клиенты и какие устройства предпочитают платящие пользователи. Постройте таблицу, отражающую количество пользователей и долю платящих для каждого устройства.
  • Изучите рекламные источники привлечения и определите каналы, из которых пришло больше всего платящих пользователей. Постройте таблицу, отражающую количество пользователей и долю платящих для каждого канала привлечения.
In [21]:
# получаем профили пользователей
profiles = get_profiles(visits, orders, costs)
profiles.head(5)
Out[21]:
user_id first_ts channel device region dt month payer acquisition_cost
0 599326 2019-05-07 20:58:57 FaceBoom Mac United States 2019-05-07 2019-05-01 True 1.088172
1 4919697 2019-07-09 12:46:07 FaceBoom iPhone United States 2019-07-09 2019-07-01 False 1.107237
2 6085896 2019-10-01 09:58:33 organic iPhone France 2019-10-01 2019-10-01 False 0.000000
3 22593348 2019-08-22 21:35:48 AdNonSense PC Germany 2019-08-22 2019-08-01 False 0.988235
4 31989216 2019-10-02 00:07:44 YRabbit iPhone United States 2019-10-02 2019-10-01 False 0.230769
In [22]:
# минимальная дата привлечения пользователей
min_analysis_date = costs['dt'].min()
min_analysis_date
Out[22]:
datetime.date(2019, 5, 1)
In [23]:
# максимальная дата привлечения пользователей
max_analysis_date = costs['dt'].max()
max_analysis_date
Out[23]:
datetime.date(2019, 10, 27)
In [24]:
# строим таблицу, отражающую количество пользователей и долю платящих из каждой страны
region_payer = profiles.groupby('region', as_index=False)\
   .agg({'user_id': 'nunique', 'payer': ['sum', 'mean']}, as_index=False)\
   .sort_values(by='region', ascending=False)
region_payer.columns = ['region','user_id', 'payer', 'share_payer']
region_payer.style.format({'share_payer': '{:.2%}'})
Out[24]:
  region user_id payer share_payer
3 United States 100002 6902 6.90%
2 UK 17575 700 3.98%
1 Germany 14981 616 4.11%
0 France 17450 663 3.80%
In [25]:
# строим круговую диаграмму по количеству платежеспособных пользователей с разбивкой по странам
plt.pie(region_payer['payer'], labels = (region_payer['region']), autopct='%1.2f%%')
plt.title('Страны')
plt.show();

Количество платежеспособных пользователей от общего количества пользователей составляет 5.9 % (8881/150008).

Самые платежеспособные пользователи из USA, их доля составляет 6.9 % (6902/100002) от общего количества пользователей, доля остальных стран: Germany, UK, France составляет около 4 %.

Доля платежеспособных пользователей от общего количества пользователей USA составляет 77.72 % (6902 100 / 8881). Остальная доля платежеспособных пользователей от общего количества пользователей составляет 22.28 % и распределилась по странам:Germany-6.94 % (616 100 / 8881), UK-7.88 % (700 100 / 8881), France- 7.46 % (663 100 / 8881).

Комментарий ревьюера ✔️: Правильно считаешь доли платящих в каждом сегменте и добавила наглядные пайчарты
In [26]:
# строим таблицу, отражающую количество пользователей и долю платящих для каждого устройства
device_payer = profiles.groupby('device', as_index=False)\
    .agg({'user_id': 'nunique', 'payer': ['sum', 'mean']}, as_index=False)\
    .sort_values(by='device', ascending=False)
device_payer.columns = ['device','user_id', 'payer', 'share_payer']
device_payer.style.format({'share_payer': '{:.2%}'})
Out[26]:
  device user_id payer share_payer
3 iPhone 54479 3382 6.21%
2 PC 30455 1537 5.05%
1 Mac 30042 1912 6.36%
0 Android 35032 2050 5.85%
In [27]:
# строим круговую диаграмму по количеству платежеспособных пользователей с разбивкой по каждому устройству
plt.pie(device_payer['payer'], labels = (device_payer['device']), autopct='%1.2f%%')
plt.title('Устройства')
plt.show();

Количество платежеспособных пользователей от общего количества пользователей составляет 5.9 % (8881/150008).

Платежеспособные пользователи предпочитают устройства: Mac - 6.36 % (1912/30042), iPhone - 6.21 % (3382/54479).

Доля платежеспособных пользователей от общего количества пользователей по устройствам составляет: iPhone - 38.08 % (3382 100 / 8881), Android - 23. 08 % (2050 100 / 8881), Mac - 21.53 % (1912 100 / 8881), PC - 17.31 % (1537 100 / 8881).

In [28]:
# строим таблицу, отражающую количество пользователей и долю платящих из каждой канала привлечения
channel_payer = profiles.groupby('channel', as_index=False)\
   .agg({'user_id': 'nunique', 'payer': ['sum', 'mean']},as_index=False)\
   .sort_values(by='channel', ascending=False)
channel_payer.columns = ['channel','user_id', 'payer', 'share_payer']
channel_payer.style.format({'share_payer': '{:.2%}'})
Out[28]:
  channel user_id payer share_payer
10 organic 56439 1160 2.06%
9 lambdaMediaAds 2149 225 10.47%
8 YRabbit 4312 165 3.83%
7 WahooNetBanner 8553 453 5.30%
6 TipTop 19561 1878 9.60%
5 RocketSuperAds 4448 352 7.91%
4 OppleCreativeMedia 8605 233 2.71%
3 MediaTornado 4364 156 3.57%
2 LeapBob 8553 262 3.06%
1 FaceBoom 29144 3557 12.20%
0 AdNonSense 3880 440 11.34%
In [29]:
# строим круговую диаграмму по количеству платежеспособных пользователей с разбивкой по канала привлечения
plt.pie(channel_payer['payer'], labels = (channel_payer['channel']), autopct='%1.2f%%')
plt.title('Каналы привлечения')
plt.show();

Количество платежеспособных пользователей от общего количества пользователей составляет 5.9 % (8881/150008).

Платежеспособные пользователи предпочитают каналы привлечения: FaceBoom - 12.20 % (3557/29144), AdNonSense - 11.34 % (440/3880), lambdaMediaAds - 10.47 % (225/2149), TipTop - 9.60 % (1878/19561), RocketSuperAds - 9.60 % (1878/19561).

Доля платежеспособных пользователей от общего количества пользователей по каналу привлечения составляет: FaceBoom - 40.05 % (3557 100 / 8881), TipTop - 21.15 % (1878 100 / 8881).

Вывод: Платежеспособные пользователи из USA предпочитают устройства Mac - 6.36 % , iPhone - 6.21 % с каналом привлечения FaceBoom - 12.20%, AdNonSense - 11.34 %, lambdaMediaAds - 10.47 %, TipTop - 9.60 % , RocketSuperAds - 9.60 % .

Маркетинг¶

  • Посчитайте общую сумму расходов на маркетинг.
  • Выясните, как траты распределены по рекламным источникам, то есть сколько денег потратили на каждый источник.
  • Постройте визуализацию динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику. Постарайтесь отразить это на одном графике.
  • Узнайте, сколько в среднем стоило привлечение одного пользователя (CAC) из каждого источника. Используйте профили пользователей.
In [30]:
profiles
Out[30]:
user_id first_ts channel device region dt month payer acquisition_cost
0 599326 2019-05-07 20:58:57 FaceBoom Mac United States 2019-05-07 2019-05-01 True 1.088172
1 4919697 2019-07-09 12:46:07 FaceBoom iPhone United States 2019-07-09 2019-07-01 False 1.107237
2 6085896 2019-10-01 09:58:33 organic iPhone France 2019-10-01 2019-10-01 False 0.000000
3 22593348 2019-08-22 21:35:48 AdNonSense PC Germany 2019-08-22 2019-08-01 False 0.988235
4 31989216 2019-10-02 00:07:44 YRabbit iPhone United States 2019-10-02 2019-10-01 False 0.230769
... ... ... ... ... ... ... ... ... ...
150003 999956196527 2019-09-28 08:33:02 TipTop iPhone United States 2019-09-28 2019-09-01 False 3.500000
150004 999975439887 2019-10-21 00:35:17 organic PC UK 2019-10-21 2019-10-01 False 0.000000
150005 999976332130 2019-07-23 02:57:06 TipTop iPhone United States 2019-07-23 2019-07-01 False 2.600000
150006 999979924135 2019-09-28 21:28:09 MediaTornado PC United States 2019-09-28 2019-09-01 False 0.205714
150007 999999563947 2019-10-18 19:57:25 organic iPhone United States 2019-10-18 2019-10-01 False 0.000000

150008 rows × 9 columns

In [31]:
#считаем общую сумму расходов на маркетинг
profiles['acquisition_cost'].sum().round(2)
Out[31]:
105497.3
In [32]:
# считаем общее количество пользователей
profiles[{'user_id': 'nunique'}].count()
C:\Users\Tima\AppData\Local\Temp\ipykernel_10564\2576958088.py:2: FutureWarning: Passing a dict as an indexer is deprecated and will raise in a future version. Use a list instead.
  profiles[{'user_id': 'nunique'}].count()
Out[32]:
user_id    150008
dtype: int64
In [33]:
# строим таблицу распределения трат по рекламным источникам
cost_channel = profiles.groupby('channel', as_index=False)\
   .agg({'user_id': 'nunique', 'acquisition_cost': ['sum']},as_index=False)\
   .sort_values(by='channel', ascending=False)
cost_channel.columns = ['channel','user_id', 'acquisition_cost']
cost_channel
Out[33]:
channel user_id acquisition_cost
10 organic 56439 0.00
9 lambdaMediaAds 2149 1557.60
8 YRabbit 4312 944.22
7 WahooNetBanner 8553 5151.00
6 TipTop 19561 54751.30
5 RocketSuperAds 4448 1833.00
4 OppleCreativeMedia 8605 2151.25
3 MediaTornado 4364 954.48
2 LeapBob 8553 1797.60
1 FaceBoom 29144 32445.60
0 AdNonSense 3880 3911.25
In [34]:
# выделяем номер месяца и номер недели 
profiles['profiles_month'] = pd.to_datetime(profiles['dt']).dt.month 
profiles['profiles_week'] = pd.to_datetime(profiles['dt']).dt.isocalendar().week
In [35]:
# строим график динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику без сглаживания данных
profiles.pivot_table(index=['profiles_month', 'profiles_week'], \
         columns = 'channel', values = 'acquisition_cost', aggfunc = 'sum')\
        .plot(grid=True, figsize=(15, 5))
plt.ylabel('Расходы на рекламную компанию, $')
plt.xlabel('Дата привлечения (месяц, неделя)')
plt.title('Динамика рекламных расходов по каналам привлечения')
plt.legend(bbox_to_anchor=(1.02, 1.0), loc='best')
plt.show()
In [36]:
# строим график динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику с сглаживанием данных
month_week = profiles.pivot_table(index=['profiles_month', 'profiles_week'], columns = 'channel', values = 'acquisition_cost', aggfunc = 'sum')
month_week = month_week.rolling(7).sum() 
month_week.plot(grid=True, figsize=(15, 5))
plt.ylabel('Расходы на рекламную компанию, $')
plt.xlabel('Дата привлечения (месяц, неделя)')
plt.title('Динамика рекламных расходов по каналам привлечения')
plt.legend(bbox_to_anchor=(1.02, 1.0), loc='best')
plt.show()

Больше всего рекламных расходов по каналам привлечения: TipTop (54751.30), FaceBoom(32445.60). Затраты на рекламу по каналам привлечения WahooNetBanner, AdNonSense равномерные. В сентябре на 35, 40 неделях затраты на TipTop, FaceBoom резко снижаются, а затем резко растут.

In [37]:
# строим таблицу распределения трат по рекламным источникам со стоимостью привлечения новых пользователей
cost_channel_organic = profiles.query('channel != "organic"')\
   .groupby('channel', as_index=False)\
   .agg({'user_id': 'nunique', 'acquisition_cost': ['sum', 'mean']},as_index=False)\
   .sort_values(by='channel', ascending=False)
cost_channel_organic.columns = ['channel','user_id', 'acquisition_cost', 'cac']
cost_channel_organic.style.format({'acquisition_cost': '{:.2f}','cac': '{:.2f}'})
Out[37]:
  channel user_id acquisition_cost cac
9 lambdaMediaAds 2149 1557.60 0.72
8 YRabbit 4312 944.22 0.22
7 WahooNetBanner 8553 5151.00 0.60
6 TipTop 19561 54751.30 2.80
5 RocketSuperAds 4448 1833.00 0.41
4 OppleCreativeMedia 8605 2151.25 0.25
3 MediaTornado 4364 954.48 0.22
2 LeapBob 8553 1797.60 0.21
1 FaceBoom 29144 32445.60 1.11
0 AdNonSense 3880 3911.25 1.01

Наибольшей стоимостью привлечения новых пользователей по рекламным источникам является TipTop - 2.80.

Вывод: Всего на маркетинг потрачено 105497.3 у.е. Больше всего рекламных расходов по каналам привлечения: TipTop (54751.30), FaceBoom(32445.60). Наибольшей стоимостью привлечения новых пользователей по рекламным источникам является TipTop - 2.80.

In [38]:
#считаем среднее CAC
avg_cac = cost_channel_organic['acquisition_cost'] /cost_channel_organic['user_id'].mean()
round(avg_cac.mean(),2)
Out[38]:
1.13

Оценим окупаемость рекламы¶

Используя графики LTV, ROI и CAC, проанализируйте окупаемость рекламы. Считайте, что на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определите самостоятельно.

  • Проанализируйте окупаемость рекламы c помощью графиков LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проверьте конверсию пользователей и динамику её изменения. То же самое сделайте с удержанием пользователей. Постройте и изучите графики конверсии и удержания.
  • Проанализируйте окупаемость рекламы с разбивкой по устройствам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проанализируйте окупаемость рекламы с разбивкой по странам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проанализируйте окупаемость рекламы с разбивкой по рекламным каналам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Ответьте на такие вопросы:
    • Окупается ли реклама, направленная на привлечение пользователей в целом?
    • Какие устройства, страны и рекламные каналы могут оказывать негативное влияние на окупаемость рекламы?
    • Чем могут быть вызваны проблемы окупаемости?
In [39]:
# задаем момент и горизонт анализа
observation_date = datetime(2019, 11, 1).date()  # момент анализа
horizon_days = 14  # горизонт анализа 

# максимальная дата привлечения
max_horizon_days =  observation_date - timedelta(days=horizon_days - 1)

print(max_horizon_days)
2019-10-19
In [40]:
#исключим пользователей с органическим трафиком

profiles = profiles.query('channel != "organic"')
In [41]:
# считаем LTV и ROI
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles, orders, observation_date, horizon_days
)

# строим графики
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days) 

По графикам LTV и ROI можно сделать выводы:

  • Реклама не окупается. ROI достигает 80%, хотя до середины июня реклама окупалась.
  • CAC растет, что говорит об увеличение рекламного бюджета.
  • На LTV влияет сезонный фактор, но и этот показатель достаточно стабилен. Значит, дело не в ухудшении качества пользователей.
In [42]:
# смотрим конверсию 

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles, orders, observation_date, horizon_days
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days) 
In [43]:
# смотрим конверсию с разбивкой по устройствам
dimensions = ['device']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles, orders, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days) 

Конверсия выше всего с разбивкой по устройствам Android, Mac, iPhone.Конверсия подвержена сезонной зависимости.

In [44]:
# смотрим конверсию с разбивкой по странам
dimensions = ['region']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles, orders, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days) 

Конверсия выше всего с разбивкой по странам в США.Конверсия подвержена сезонной зависимости.

In [45]:
# смотрим конверсию с разбивкой по источникам привлечения
dimensions = ['channel']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles, orders, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days) 

Конверсия выше всего с разбивкой по источникам привлечения FaceBoom, AdNonSense, lambdaMediaAds, TipTop.Конверсия подвержена сезонной зависимости.

Конверсия не падает — значит, при расчёте были верно определены момент и горизонт анализа. Количество новых покупателей не может быть меньше значения предыдущего дня, ведь при расчёте используется сумма с накоплением. Выше всего конверсия: с разбивкой по странам в США; с разбивкой по устройствам Android, Mac, iPhone; по источникам привлечения FaceBoom, AdNonSense, lambdaMediaAds, TipTop.Конверсия подвержена сезонной зависимости.

In [46]:
# смотрим удержание 

retention_raw, retention_grouped, retention_history = get_retention(
    profiles, visits, observation_date, horizon_days
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days) 
In [47]:
# смотрим удержание с разбивкой по устройствам
dimensions = ['device']
retention_raw, retention_grouped, retention_history = get_retention(
    profiles, visits, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days) 

Лучше всего удерживается с разбивкой по устройствам PC, хуже Android, Mac, iPhone. Удержание подвержено сезонной зависимости.

In [48]:
# смотрим удержание с разбивкой по странам
dimensions = ['region']
retention_raw, retention_grouped, retention_history = get_retention(
    profiles, visits, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days) 

Лучше всего удерживается с разбивкой по странам UK, Germany, France, хуже USA. Удержание подвержено сезонной зависимости.

In [49]:
# смотрим удержание с разбивкой по источникам привлечения
dimensions = ['channel']
retention_raw, retention_grouped, retention_history = get_retention(
    profiles, visits, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days) 

Удержание платящих пользователей с разбивкой по источникам привлечения одинакого, кроме FaceBoom, AdNonSense. Удержание подвержено сезонной зависимости.

Retention Rate - коэффициент удержания со временем падает как у платящих пользователей, так и не платящих пользователей. Это нормально, так как коэффициент удержания показывает, сколько пользователей из когорты относительно их изначального числа вернулись, то есть воспользовались продуктом или услугой, в последующие периоды.

Лучше всего удерживается с разбивкой по устройствам PC, хуже Android, Mac, iPhone. Лучше всего удерживается с разбивкой по странам UK, Germany, France, хуже USA. Удержание платящих пользователей с разбивкой по источникам привлечения одинакого, кроме FaceBoom, AdNonSense.У платящих пользователей выявлена сезонная зависимость.

In [50]:
# смотрим окупаемость с разбивкой по устройствам

dimensions = ['device']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles, orders, observation_date, horizon_days, dimensions=dimensions
)
# строим график
plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

График с разбивкой по устройствам показывает:

  • Не окупается ни одно устройство. Хотя на графике динамики ROI пользователей видно, что в июне месяца реклама окупалась на устройствах: Android, Mac, iPhone. До сентября месяца окупалось реклама на устройстве PC;
  • LTV всё так же подвержен сезонности, но стабилен;
  • CAC растет, что говорит об увеличение рекламного бюджета.Больше всего средств было потрачено на Mac, iPhone.
In [51]:
# смотрим окупаемость с разбивкой по странам

dimensions = ['region']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles, orders, observation_date, horizon_days, dimensions=dimensions
)
# строим график
plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

График с разбивкой по странам показывает:

  • Реклама окупается UK, Germany, France. Хотя на графике динамики ROI пользователей видно, что в июне месяца реклама окупалась во всех странах;
  • Стоимость привлечения в USA растет в отличии от других стран, у которых CAC падает;
  • LTV больше всего у USA. Все страны подвержены сезонности, но стабильны.
In [52]:
# смотрим окупаемость с разбивкой по источникам привлечения

dimensions = ['channel']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles, orders, observation_date, horizon_days, dimensions=dimensions
)
# строим график
plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

График с разбивкой по источникам привлечения показывает:

  • Реклама окупается YRabbit, lambdaMediaAds, WahooNetBanner, RocketSuperAds, OppleCreativeMedia, MediaTornado, LeapBob. В июле месяце YRabbit была намного выше остальных.На графике ROI и динамики ROI пользователей видно, что реклама не окупались по источникам привлечения: FaceBoom, AdNonSense и TipTop;
  • Стоимость привлечения по источникам привлечения TipTop сильно растет, в отличие от других источников, у которых CAC стабилен;
  • LTV больше всего у lambdaMediaAds и TipTop. Все источникам привлечения подвержены сезонности, но стабильны.

Вывод:

  • В данный момент на привлечение пользователей реклама не окупается, потому что расходы на рекламу составляют 105497.30 у.е. привлечено пользователй 150008 у.е. Убыток составляет 44510,70 у.е.Расходы на рекламу растут, а динамика привлечения пользователей не меняется;
  • Негативное влияние на окупаемость рекламы оказывают все устройства (Android, Mac, iPhone, PC) по источникам привлечения (FaceBoom, AdNonSense и TipTop) из стран USA.

Проблема окупаемости вызвана с увеличением расходов на рекламу и малым количеством привлечения пользователей.

In [53]:
# оставляем в таблице США
profiles_usa = profiles.query('region == "United States"')
In [54]:
# смотрим окупаемость с разбивкой по источникам привлечения
dimensions = ['channel']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles_usa, orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

В США динамика стоимости привлечения пользователей через TipTop была значительно выше, чем у других каналов, но расходы на рекламу не окупились.

In [55]:
# смотрим окупаемость с разбивкой по устройствам по стране США
dimensions = ['device']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles_usa, orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

В США динамика стоимости привлечения пользователей была практически одинаковой для всех устройств. Ни одно устройство не окупилось.

In [56]:
# исключаем из таблицы США
profiles_not_usa = profiles.query('region != "United States"')
In [57]:
# смотрим окупаемость с разбивкой по источникам привлечения без США
dimensions = ['channel']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles_not_usa, orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

График с разбивкой по источникам привлечения показывает:

  • Реклама окупается lambdaMediaAds, WahooNetBanner, OppleCreativeMedia, LeapBob. На графике ROI и динамики ROI пользователей видно, что реклама не окупались по источникам привлечения: AdNonSense;
  • CAC у всем источникам привлечения стабилен;
  • LTV больше всего у lambdaMediaAds и TipTop. Все источникам привлечения подвержены сезонности, но стабильны.
In [58]:
# смотрим окупаемость с разбивкой по устройствам без США
dimensions = ['device']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles_not_usa, orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

Окупаются все устройства, LTV всё так же подвержен сезонности, но стабилен, CAC уменьшается по всем устройствам.

Еще раз убедились, что все проблемы в США. Все бизнес-паказатели пришли в норму. Реклама окупается по всем устройствам и источникам привлечения. Кроме канала привлечения AdNonSense.

Напишите выводы¶

  • Выделите причины неэффективности привлечения пользователей.
  • Сформулируйте рекомендации для отдела маркетинга.

Вывод:

В исследовании проанализированы данные по пользователям развлекательного приложения Procrastinate Pro+ из США, Великобритания, Франция, Германия., разделенные на категории устройств (iPhone, Android, PC, Mac) и разных каналах привлечения. В данный момент на привлечение пользователей реклама не окупается, потому что расходы на рекламу составляют 105497.30 у.е., привлечено пользователей 150008 у.е. Убыток составляет 44510,70 у.е.Расходы на рекламу растут, а динамика привлечения пользователей не меняется.

Большую долю платежеспособных пользователей от общего количества пользователей занимает США(77.72%), которая оказывает негативное влияние на окупаемость приложения. Неоправданные расходы на рекламу были направлены на устройства: Mac, iPhone по источникам привлечения TipTop.

Рекомендации

Необходимо сократить финансирование на рекламу в USA устройств iPhone, Mac по каналам привлечения TipTop. Снизить расходы на рекламу по каналам привлечения FaceBoom и AdNonSense, т.к. ежемесячные траты на рекламу стабильны, но это не помогает привлечь новых пользователей. Увеличить рекламу устройств Android, PC для привлечения новых пользователей.